/*
 * MIT License
 *
 * Copyright (c) 2024-2025 milkpotatoes
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#ifndef SRC_UTILS_INCLUDE_NAPI_UTILS_H
#define SRC_UTILS_INCLUDE_NAPI_UTILS_H

#include <cstdint>
#include <iterator>
#include <string>
#include <vector>

#include "napi/native_api.h" // IWYU pragma: keep

#define IS_SPECIAL_NAPI_VALUE_LIST(XX)            \
    XX(array, Array)                              \
    XX(error, Error)                              \
    XX(arraybuffer, ArrayBuffer)                  \
    XX(typedarray, TypedArray)                    \
    XX(dataview, DataView)                        \
    XX(promise, Promise)                          \
    XX(date, Date)                                \
    XX(detached_arraybuffer, DetachedArrayBuffer) \
    XX(buffer, Buffer)                            \
    /* Only in OH */                              \
    XX(sendable, Sendable)                        \
    XX(concurrent_function, ConcurrentFunction)

#define IS_BASIC_NAPI_TYPE_LIST(XX) \
    XX(undefined, Undefined)        \
    XX(null, Null)                  \
    XX(boolean, Boolean)            \
    XX(number, Number)              \
    XX(string, String)              \
    XX(symbol, Symbol)              \
    XX(object, Object)              \
    XX(function, Function)          \
    XX(external, External)          \
    XX(bigint, Bigint)

#define COERCE_TO_TYPE_LIST(XX) \
    XX(bool, Boolean)           \
    XX(number, Number)          \
    XX(object, Object)          \
    XX(string, String)

namespace utils
{
napi_value GetUndefined(napi_env env);
napi_value GetGlobal(napi_env env);

class NapiCallbackInfo
{
public:
    NapiCallbackInfo(napi_env env, napi_callback_info info) : env_(env)
    {
        napi_get_cb_info(env, info, &argc_, nullptr, &thisVar_, &data_);
        if (argc_ > 0)
        {
            argv_ = new napi_value[argc_];
            napi_get_cb_info(env, info, &argc_, argv_, nullptr, nullptr);
        }
    }
    ~NapiCallbackInfo()
    {
        if (argc_ > 0)
        {
            delete[] argv_;
        }
    }

    // clang-format off
    inline napi_env GetEnv() const { return env_; }
    inline size_t GetArgc() const { return argc_; }
    inline napi_value *GetArgs() const { return argv_; }

    inline napi_value GetArg(size_t index) const
    {
        if (index >= argc_)
        {
            return GetUndefined(env_);
        }
        return argv_[index];
    }
    inline napi_value operator[](size_t index) const
    {
        return GetArg(index);
    }

    inline napi_value GetThisVar() const { return thisVar_; }
    inline void *GetData() const { return data_; }
    // clang-format on

private:
    napi_env env_{nullptr};
    size_t argc_{0};
    napi_value *argv_{nullptr};
    napi_value thisVar_{nullptr};
    void *data_{nullptr};
};

class TryCatch
{
public:
    explicit TryCatch(napi_env env) : env_(env)
    {}
    bool HasCaught()
    {
        bool caught = false;
        if (napi_is_exception_pending(env_, &caught) != napi_ok)
        {
            return true;
        };
        return caught;
    }
    napi_value Clear()
    {
        napi_value exception = nullptr;
        if (napi_get_and_clear_last_exception(env_, &exception) != napi_ok)
        {
            return nullptr;
        }
        return exception;
    }

private:
    napi_env env_{nullptr};
};

class NapiHandleScope
{
public:
    explicit NapiHandleScope(napi_env env) : env_(env)
    {
        napi_open_handle_scope(env, &scope_);
    };
    ~NapiHandleScope()
    {
        napi_close_handle_scope(env_, scope_);
        scope_ = nullptr;
    }

private:
    const napi_env env_{nullptr};
    napi_handle_scope scope_{nullptr};
};

class NapiEscapableHandleScope
{
public:
    explicit NapiEscapableHandleScope(napi_env env) : env_(env)
    {
        napi_open_escapable_handle_scope(env, &scope_);
    };
    ~NapiEscapableHandleScope()
    {
        napi_close_escapable_handle_scope(env_, scope_);
        scope_ = nullptr;
    }
    napi_value Escape(napi_value value)
    {
        napi_value result = nullptr;
        if (napi_escape_handle(env_, scope_, value, &result) != napi_ok)
        {
            return nullptr;
        }
        return result;
    }

private:
    const napi_env env_{nullptr};
    napi_escapable_handle_scope scope_{nullptr};
};

napi_value GetProperty(napi_env env, napi_value object, napi_value key);
napi_value GetProperty(napi_env env, napi_value object, const std::string &key);
napi_value GetProperty(napi_env env, napi_value object, const char *key);

bool HasProperty(napi_env env, napi_value object, napi_value key);
bool HasProperty(napi_env env, napi_value object, const std::string &key);
bool HasProperty(napi_env env, napi_value object, const char *key);

#define IS_SPECIAL_NAPI_VALUE_BODY(method, name)              \
    inline bool Is##name(napi_env env, napi_value value)      \
    {                                                         \
        bool result = false;                                  \
        if (napi_is_##method(env, value, &result) != napi_ok) \
        {                                                     \
            return false;                                     \
        }                                                     \
        return result;                                        \
    }
IS_SPECIAL_NAPI_VALUE_LIST(IS_SPECIAL_NAPI_VALUE_BODY)
#undef IS_SPECIAL_NAPI_VALUE_BODY
#undef IS_SPECIAL_NAPI_VALUE_LIST

inline napi_valuetype TypeOf(napi_env env, napi_value value)
{
    napi_valuetype result;
    if (napi_typeof(env, value, &result) != napi_ok)
    {
        errno = -1;
        return napi_undefined;
    }
    return result;
}

#define IS_BASIC_NAPI_TYPE_BODY(type, name)              \
    inline bool Is##name(napi_env env, napi_value value) \
    {                                                    \
        napi_valuetype result;                           \
        if (napi_typeof(env, value, &result) != napi_ok) \
        {                                                \
            return false;                                \
        }                                                \
        return result == napi_##type;                    \
    }
IS_BASIC_NAPI_TYPE_LIST(IS_BASIC_NAPI_TYPE_BODY)

inline const char *ReadNapiValuetype(napi_valuetype type)
{
    switch (type)
    {
#define RETURN_NAPI_TYPE_NAME(name, _) \
    case napi_##name:                  \
        return #name;
        IS_BASIC_NAPI_TYPE_LIST(RETURN_NAPI_TYPE_NAME)
#undef RETURN_NAPI_TYPE_NAME
    default:
        return "unknown";
    }
}

#undef IS_BASIC_NAPI_TYPE_BODY
#undef IS_BASIC_NAPI_TYPE_LIST

#define COERCE_TO_TYPE_BODY(type, name)                              \
    inline napi_value CoerceTo##name(napi_env env, napi_value value) \
    {                                                                \
        napi_value result;                                           \
        if (napi_coerce_to_##type(env, value, &result) != napi_ok)   \
        {                                                            \
            return nullptr;                                          \
        }                                                            \
        return result;                                               \
    }
COERCE_TO_TYPE_LIST(COERCE_TO_TYPE_BODY)
#undef COERCE_TO_TYPE_BODY
#undef COERCE_TO_TYPE_LIST

#define NAPI_NUMBER_LIST(XX)     \
    XX(Int32, int32_t, int32)    \
    XX(Uint32, uint32_t, uint32) \
    XX(Int64, int64_t, int64)    \
    XX(Double, double, double)

#define CREATE_NUMBER_BODY(name, type, api)                  \
    inline napi_value Create##name(napi_env env, type value) \
    {                                                        \
        napi_value result = nullptr;                         \
        napi_create_##api(env, value, &result);              \
        return result;                                       \
    }

NAPI_NUMBER_LIST(CREATE_NUMBER_BODY)

#undef CREATE_NUMBER_BODY

#define GET_VALUE_NUMBER_BODY(name, type, api)                 \
    inline type GetValue##name(napi_env env, napi_value value) \
    {                                                          \
        type result;                                           \
        napi_get_value_##api(env, value, &result);             \
        return result;                                         \
    }

NAPI_NUMBER_LIST(GET_VALUE_NUMBER_BODY)

#undef GET_VALUE_NUMBER_BODY
#undef NAPI_NUMBER_LIST

#define NAPI_STRING_LIST(XX)         \
    XX(Latin1, const char *, latin1) \
    XX(Utf8, const char *, utf8)     \
    XX(Utf16, const char16_t *, utf16)

#define CREATE_STRING_BODY(name, type, api)                                                          \
    inline napi_value CreateString##name(napi_env env, type value, size_t length = NAPI_AUTO_LENGTH) \
    {                                                                                                \
        napi_value result = nullptr;                                                                 \
        napi_create_string_##api(env, value, length, &result);                                       \
        return result;                                                                               \
    }
NAPI_STRING_LIST(CREATE_STRING_BODY)

#undef CREATE_STRING_BODY
#undef NAPI_STRING_LIST

#define NAPI_CPP_STRING_LIST(XX)           \
    XX(const std::string_view &, utf8)     \
    XX(const std::string &, utf8)          \
    XX(const std::u16string_view &, utf16) \
    XX(const std::u16string &, utf16)
#define CREATE_CPP_STRING_BODY(type, api)                               \
    inline napi_value CreateString(napi_env env, type str)              \
    {                                                                   \
        napi_value result = nullptr;                                    \
        napi_create_string_##api(env, str.data(), str.size(), &result); \
        return result;                                                  \
    }
NAPI_CPP_STRING_LIST(CREATE_CPP_STRING_BODY)
#undef CREATE_CPP_STRING_BODY
#undef NAPI_CPP_STRING_LIST

napi_value CreateBigInt(napi_env env, int64_t value);
napi_value CreateBigInt(napi_env env, uint64_t value);

inline napi_value CreateBigIntInt64(napi_env env, int64_t value)
{
    return CreateBigInt(env, value);
}
inline napi_value CreateBigIntUint64(napi_env env, uint64_t value)
{
    return CreateBigInt(env, value);
}

uint64_t GetValueBigIntUint64(napi_env env, napi_value value, bool &lossless);
int64_t GetValueBigIntInt64(napi_env env, napi_value value, bool &lossless);

napi_value ToString(napi_env env, napi_value value);

inline std::string GetValueStringUtf8(napi_env env, napi_value value)
{
    size_t length = 0;
    if (napi_get_value_string_utf8(env, value, nullptr, 0, &length) != napi_ok)
        return "";
    std::string str;
    str.resize(length);
    napi_get_value_string_utf8(env, value, str.data(), str.size() + 1, nullptr);
    return str;
}

inline napi_value CreateObject(napi_env env)
{
    napi_value result = nullptr;
    napi_create_object(env, &result);
    return result;
}
inline napi_value CreateArray(napi_env env, uint32_t length = 0)
{
    napi_value result = nullptr;
    if (length == 0)
        napi_create_array(env, &result);
    else
        napi_create_array_with_length(env, length, &result);
    return result;
}

void ThrowInvalidArg(napi_env env, const char *msg);
bool ThrowIfTypeError(napi_env env, const char *msg, const char *argName, napi_value arg, napi_valuetype type);

inline bool GetValueBool(napi_env env, napi_value value)
{
    bool result = false;
    napi_get_value_bool(env, value, &result);
    return result;
}

inline bool HasElement(napi_env env, napi_value array, uint32_t index)
{
    bool result = false;
    napi_has_element(env, array, index, &result);
    return result;
}

inline napi_value GetElement(napi_env env, napi_value array, uint32_t index)
{
    napi_value result = nullptr;
    napi_get_element(env, array, index, &result);
    return result;
}

inline uint32_t GetArrayLength(napi_env env, napi_value array)
{
    uint32_t length = 0;
    napi_get_array_length(env, array, &length);
    return length;
}

struct WrapData
{
    bool runFinalize_;
    void *data_;
    napi_finalize finalize_;
};

bool Wrap(napi_env, napi_value object, void *data, napi_finalize finalize = nullptr, void *hint = nullptr);

void *Unwrap(napi_env, napi_value object);

void *RemoveWrap(napi_env, napi_value object);

class GlobalSymbol
{
public:
    GlobalSymbol(napi_env env, std::string name);
    ~GlobalSymbol();
    napi_value GetValue();

private:
    static void CleanUp(void *arg);
    napi_env env_{nullptr};
    napi_ref ref_{nullptr};
};

inline bool InstanceOf(napi_env env, napi_value value, napi_value cons)
{
    bool result = false;
    if (!napi_instanceof(env, value, cons, &result))
        return false;
    return result;
}

void SetProperty(napi_env env, napi_value object, napi_value key, napi_value value, napi_property_attributes attr);
void SetProperty(napi_env env, napi_value object, const char *name, napi_value value, napi_property_attributes attr);
void SetProperty(napi_env env, napi_value object, std::string name, napi_value value, napi_property_attributes attr);
void SetProperty(napi_env env, napi_value object, std::string_view name, napi_value value,
                 napi_property_attributes attr);
void SetUnenumerableProperty(napi_env env, napi_value object, const char *name, napi_value value);
void SetUnenumerableProperty(napi_env env, napi_value object, napi_value key, napi_value value);

uv_loop_s *GetUVLoop(napi_env env);

#define DECL_NAPI_CALLBACK(name)                                             \
    inline static napi_value name(napi_env _env_, napi_callback_info _info_) \
    {                                                                        \
        utils::NapiCallbackInfo info(_env_, _info_);                         \
        return name##Inner(info);                                            \
    }                                                                        \
    static napi_value name##Inner(const utils::NapiCallbackInfo &info)

#define DEF_NAPI_CALLBACK(name, info) napi_value name##Inner(const utils::NapiCallbackInfo &(info))

#define RUNNER_NAPI_CALL(env, call, returnVal)                                   \
    if ((call) != napi_ok)                                                       \
    {                                                                            \
        napi_throw_error((env), "Runtime error", "failed to invoke " #call "."); \
        return (returnVal);                                                      \
    }

#define RUNNER_UV_CALL(env, call, returnVal)                                     \
    if ((call) != 0)                                                             \
    {                                                                            \
        napi_throw_error((env), "Runtime error", "failed to invoke " #call "."); \
        return (returnVal);                                                      \
    }

inline napi_ref CreateReference(napi_env env, napi_value value, uint32_t refCount = 1)
{
    napi_ref result = nullptr;
    napi_create_reference(env, value, refCount, &result);
    return result;
}

inline napi_value GetReferenceValue(napi_env env, napi_ref ref)
{
    napi_value result = nullptr;
    napi_get_reference_value(env, ref, &result);
    return result;
}

inline bool IsExceptionPending(napi_env env)
{
    bool result = false;
    if (napi_is_exception_pending(env, &result) != napi_ok)
    {
        return true;
    }
    return result;
}

template <typename T> using ArrayElementCastFunction = T (*)(napi_env env, napi_value elem);
template <typename Iterator, typename UnaryOperation>
inline void Transform(napi_env env, napi_value array, Iterator out, UnaryOperation castFn)
{
    auto length = GetArrayLength(env, array);
    for (uint32_t i = 0; i < length; i++, out++)
    {
        auto el = GetElement(env, array, i);
        *out = castFn(env, el);
    }
}

template <typename T, typename UnaryOperation>
inline std::vector<T> ToVector(napi_env env, napi_value array, UnaryOperation castFn)
{
    std::vector<T> result;
    result.reserve(GetArrayLength(env, array));
    Transform(env, array, std::back_inserter(result), castFn);
    return result;
}

inline std::vector<napi_value> ToVector(napi_env env, napi_value array)
{
    return ToVector<napi_value>(env, array, []([[maybe_unused]] napi_env env, napi_value x) { return x; });
}

template <typename Iterator, typename UnaryOperation>
inline napi_value ToArray(napi_env env, Iterator begin, Iterator end, UnaryOperation fn)
{
    auto array = CreateArray(env);
    uint32_t index = 0;
    for (auto it = begin; it != end; it++, index++)
    {
        napi_set_element(env, array, index, fn(env, *it));
    }
    return array;
}

napi_value CreateExternalArrayBuffer(napi_env env, void *data, size_t length, napi_finalize finalize,
                                     void *finalizeHint = nullptr);
napi_value CreateArrayBuffer(napi_env env, size_t length, void *&data);

} // namespace utils

#endif /* SRC_UTILS_INCLUDE_NAPI_UTILS_H */